title: git文档7 - Git 工具
date: 2020.11.14
top:
tags:
7.1 选择修订版本
7.2 交互式暂存
7.3 贮藏与清理
7.4 签署工作
7.5 搜索
7.6 重写历史
7.7 重置揭密
7.8 高级合并
7.9 Rerere
7.10 使用 Git 调试
7.11 子模块
7.12 打包
7.13 替换
7.14 凭证存储
7.15 总结
2020.11,14 星期六 18:12
Git 能够以多种方式来指定单个提交、一组提交、或者一定范围内的提交。
你可以通过任意一个提交的 40 个字符的完整 SHA-1 散列值来指定它, 不过
git log; git show xx23
如果你在 git log 后加上 --abbrev-commit
参数,输出结果里就会显示简短且唯一的值;git log --abbrev-commit --pretty=oneline
引用特定提交的一种直接方法是,若它是一个分支的顶端的提交, 那么可以在任何需要引用该提交的 Git 命令中直接使用该分支的名称。
git rev-parse topic1
每当你的 HEAD 所指向的位置发生了变化,Git 就会将这个信息存储到引用日志这个历史记录里。
这个方法只对还在你引用日志里的数据有用,所以不能用来查好几个月之前的提交。
git reflog
git show HEAD@{5}
git show master@{yesterday}
git log -g master # 来查看类似于 git log 输出格式的引用日志信息
而区别在于你在后面加数字的时候。 HEAD~2 代表“第一父提交的第一父提交”,也就是“祖父提交”——Git 会根据你指定的次数获取对应的第一父提交。
查看 experiment 分支中还有哪些提交尚未被合并入 master 分支。(在 experiment 分支中而不在 master 分支中的提交)git log master..experiment
另一个常用的场景是查看你即将推送到远端的内容:git log origin/master..HEAD
sh
$ git log refA refB ^refC
$ git log refA refB --not refC
git log master...experiment
git log --left-right master...experiment
git add -p
或 git add --patch
来启动同样的脚本。git reset --patch
命令的补丁模式来部分重置文件,git checkout --patch
命令来部分检出文件git stash save --patch
命令来部分暂存文件。git stash apply --index
文件的改动被重新应用了,但是之前暂存的文件却没有重新暂存。 想要那样的话,必须使用 –index 选项来运行 git stash apply 命令,来尝试重新应用暂存的修改。
第一个非常流行的选项是 git stash 命令的 –keep-index 选项。 它告诉 Git 不仅要贮藏所有已暂存的内容,同时还要将它们保留在索引中。
另一个经常使用贮藏来做的事情是像贮藏跟踪文件一样贮藏未跟踪文件。
如果指定 –include-untracked 或 -u 选项,Git 也会贮藏任何未跟踪文件。
在贮藏中包含未跟踪的文件仍然不会包含明确 忽略 的文件。 要额外包含忽略的文件,请使用 –all 或 -a 选项。
最终,如果指定了 –patch 标记,Git 不会贮藏所有修改过的任何东西, 但是会交互式地提示哪些改动想要贮藏、哪些改动需要保存在工作目录中。
git stash branch <new branchname>
清理工作目录有一些常见的原因,比如说为了移除由合并或外部工具生成的东西, 或是为了运行一个干净的构建而移除之前构建的残留。
你需要谨慎地使用这个命令,因为它被设计为从工作目录中移除未被追踪的文件。
你可以使用 git clean 命令去除冗余文件或者清理工作目录。 使用 git clean -f -d 命令来移除工作目录中所有未追踪的文件以及空的子目录。
如果只是想要看看它会做什么,可以使用 –dry-run 或 -n 选项来运行命令, 这意味着“做一次演习然后告诉你 将要 移除什么”。
默认情况下,git clean 命令只会移除没有忽略的未跟踪文件。 任何与 .gitignore 或其他忽略文件中的模式匹配的文件都不会被移除。 如果你也想要移除那些文件, 可以给 clean 命令增加一个 -x 选项。
如果不知道 git clean 命令将会做什么,在将 -n 改为 -f 来真正做之前总是先用 -n 来运行它做双重检查。
另一个小心处理过程的方式是使用 -i 或 “interactive” 标记来运行它。
GPG 介绍
签署标签git tag -s v1.5 -m 'my signed 1.5 tag'
验证标签git tag -v <tag-name>
签署提交git commit -a -S -m 'signed commit'
也可以给 git merge 命令附加 -S 选项来签署自己生成的合并提交。
每个人必须签署
签署标签与提交很棒,但是如果决定在正常的工作流程中使用它,你必须确保团队中的每一个人都理解如何这样做。 如果没有,你将会花费大量时间帮助其他人找出并用签名的版本重写提交。 在采用签署成为标准工作流程的一部分前,确保你完全理解 GPG 及签署带来的好处。
默认情况下 git grep 会查找你工作目录的文件。 第一种变体是,你可以传递 -n 或 –line-number 选项数来输出 Git 找到的匹配行的行号。
若不想打印所有匹配的项,你可以使用 -c 或 –count 选项来让 git grep 输出概述的信息, 其中仅包括那些包含匹配字符串的文件,以及每个文件中包含了多少个匹配。
如果你还关心搜索字符串的 上下文,那么可以传入 -p 或 –show-function 选项来显示每一个匹配的字符串所在的方法或函数:
git log -S ZLIB_BUF_MAX --oneline
git log -L :git_deflate_bound:zlib.c
git log -L '/unsigned long git_deflate_bound/',/^}/:zlib.c
git commit --amend
git commit --amend --no-edit
通过交互式变基工具,可以在任何想要修改的提交后停止,然后修改信息、添加文件或做任何想做的事情。
可以通过给 git rebase 增加 -i 选项来交互式地运行变基。 必须指定想要重写多久远的历史,这可以通过告诉命令将要变基到的提交来做到。git rebase -i HEAD~3
再次记住这是一个变基命令——在 HEAD~3..HEAD 范围内的每一个修改了提交信息的提交及其 所有后裔 都会被重写。
不要涉及任何已经推送到中央服务器的提交
运行这个命令会在文本编辑器上给你一个提交的列表,看起来像下面这样:
pick f7f3f6d changed my name a bit
pick 310154e updated README formatting and added blame
pick a5f4a0d added cat-file
# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out
需要重点注意的是相对于正常使用的 log 命令,这些提交显示的顺序是相反的。
你只要将你想修改的每一次提交前面的 ‘pick’ 改为 ‘edit’。
当保存并退出编辑器时,git commit --amend
; git rebase --continue
$_PS: 以下都是通过 -i 的交互操作命令来编辑。
如果,指定 “squash” 而不是 “pick” 或 “edit”
全局修改你的邮箱地址或从每一个提交中移除一个文件。
除非你的项目还没有公开并且其他人没有基于要改写的工作的提交做的工作,否则你不应当使用它。
git filter-branch 有很多陷阱,不再推荐使用它来重写历史。 请考虑使用 git-filter-repo
从每一个提交中移除一个文件git filter-branch --tree-filter 'rm -f passwords.txt' HEAD
使一个子目录做为新的根目录
全局修改邮箱地址
$_PS: 既核武器,就慎用吧。
2.4 Git 基础 - 撤消操作
$ git reset HEAD CONTRIBUTING.md
Unstaged changes after reset:
M CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
renamed: README.md -> README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: CONTRIBUTING.md
git reset 确实是个危险的命令,如果加上了 –hard 选项则更是如此。 然而在上述场景中,工作目录中的文件尚未修改,因此相对安全一些。
请务必记得git checkout -- <file>
是一个危险的命令。 你对那个文件在本地的任何修改都会消失——Git 会用最近提交的版本覆盖掉它。 除非你确实清楚不想要对那个文件的本地修改了,否则请不要使用这个命令。
HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。
Index 索引是你的 预期的下一次提交。 我们也会将这个概念引用为 Git 的“暂存区”,这就是当你运行 git commit 时 Git 看起来的样子。
Working Directory 工作目录(通常也叫 工作区).沙盒
让我们跟着 reset 看看它都做了什么。 它以一种简单可预见的方式直接操纵这三棵树。 它做了三个基本操作。
第 1 步:移动 HEAD
无论你调用了何种形式的带有一个提交的 reset,它首先都会尝试这样做。 使用 reset –soft,它将仅仅停在那儿。
现在你可以更新索引并再次运行 git commit 来完成 git commit –amend 所要做的事情了(见 修改最后一次提交)。
第 2 步:更新索引(–mixed)
如果指定 –mixed 选项,reset 将会在这时停止。
它依然会撤销一上次 提交,但还会 取消暂存 所有的东西。 于是,我们回滚到了所有 git add 和 git commit 的命令执行之前。
第 3 步:更新工作目录(–hard)
reset 要做的的第三件事情就是让工作目录看起来像索引。 如果使用 –hard 选项,它将会继续这一步。
回顾
reset 命令会以特定的顺序重写这三棵树,在你指定以下选项时停止:
移动 HEAD 分支的指向 (若指定了 –soft,则到此停止)
使索引看起来像 HEAD (若未指定 –hard,则到此停止)
使工作目录看起来像索引
若指定了一个路径,reset 将会跳过第 1 步,并且将它的作用范围限定为指定的文件或文件集合。
这样做自然有它的道理,因为 HEAD 只是一个指针,你无法让它同时指向两个提交中各自的一部分。 不过索引和工作目录 可以部分更新,所以重置会继续进行第 2、3 步。
现在,假如我们运行 git reset file.txt
(这其实是 git reset --mixed HEAD file.txt
的简写形式,因为你既没有指定一个提交的 SHA-1 或分支,也没有指定 –soft 或 –hard),它会:
移动 HEAD 分支的指向 (已跳过)
让索引看起来像 HEAD (到此处停止)
所以它本质上只是将 file.txt 从 HEAD 复制到索引中。
它还有 取消暂存文件 的实际效果。git reset eb43bf file.txt
还有一点同 git add 一样,就是 reset 命令也可以接受一个 –patch 选项来一块一块地取消暂存的内容。
可以运行 git reset --soft HEAD~2
来将 HEAD 分支移动到一个旧一点的提交上(即你想要保留的最近的提交)
然后只需再次运行 git commit:
和 reset 一样,checkout 也操纵三棵树,不过它有一点不同,这取决于你是否传给该命令一个文件路径。
运行 git checkout [branch]
与运行git reset --hard [branch]
非常相似,它会更新所有三棵树使其看起来像 [branch]
,
首先不同于 reset –hard,checkout 对工作目录是安全的,
其实它还更聪明一些。
第二个重要的区别是 checkout 如何更新 HEAD。 reset 会移动 HEAD 分支的指向,而 checkout 只会移动 HEAD 自身来指向另一个分支。
运行 checkout 的另一种方式就是指定一个文件路径,这会像 reset 一样不会移动 HEAD。
下面的速查表列出了命令对树的影响。 “HEAD” 一列中的 “REF” 表示该命令移动了 HEAD 指向的分支引用,而 “HEAD” 则表示只移动了 HEAD 自身。
特别注意 WD Safe? 一列——如果它标记为 NO,那么运行该命令之前请考虑一下。
Commit Level
– | HEAD | Index | Workdir | WD Safe?
– | – | – | – | –reset --soft [commit]
| REF | NO | NO | YESreset [commit]
| REF | YES | NO | YESreset --hard [commit]
| REF | YES | YES | NOcheckout <commit>
| HEAD| YES| YES| YES
File Level
– | HEAD | Index | Workdir | WD Safe?
– | – | – | – | –reset [commit] <paths>
| NO | YES | NO | YES
checkout [commit] <paths>
| NO| YES| YES| NO
现在我们尝试合并入我们的 whitespace 分支,因为修改了空白字符,所以合并会出现冲突。
git merge --abort
选项会尝试恢复到你运行合并前的状态。
但当运行命令前,在工作目录中有未储藏、未提交的修改时它不能完美处理,除此之外它都工作地很好。
如果出于某些原因你想要重来一次,也可以运行 git reset –hard HEAD 回到上一次提交的状态。
-Xignore-all-space
或 -Xignore-space-change
选项。 第一个选项在比较行时 完全忽略 空白修改,git merge -Xignore-space-change whitespace
然后我们想要我的版本的文件,他们的版本的文件(从我们将要合并入的分支)和共同的版本的文件(从分支叉开时的位置)的拷贝。
然后我们想要修复任何一边的文件,并且为这个单独的文件重试一次合并。
我们可以手工修复它们来修复空白问题,然后使用鲜为人知的 git merge-file
命令来重新合并那个文件。
# 通过 git show 命令与一个特别的语法,你可以将冲突文件的这些版本释放出一份拷贝。
$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb
# 如果你想要更专业一点,也可以使用 `ls-files -u` 底层命令来得到这些文件的 Git blob 对象的实际 SHA-1 值。
# :1:hello.rb 只是查找那个 blob 对象 SHA-1 值的简写。
$ git merge-file -p \
hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb
实际上,这比使用 ignore-space-change 选项要更好,因为。。。
如果你想要在最终提交前看一下我们这边与另一边之间实际的修改, 你可以使用 git diff 来比较将要提交作为合并结果的工作目录与其中任意一个阶段的文件差异。 让我们看看它们。
# 看看合并引入了什么,可以运行 git diff --ours
$ git diff --ours
# 如果我们想要查看合并的结果与他们那边有什么不同,可以运行 git diff --theirs。
# 在本例及后续的例子中,我们会使用 -b 来去除空白
$ git diff --theirs -b
# 最终,你可以通过 git diff --base 来查看文件在两边是如何改动的。
$ git diff --base -b
# 在这时我们可以使用 git clean 命令来清理我们为手动合并而创建但不再有用的额外文件。
$ git clean -f
$_PS: 有编辑器插件。手动文件和检出冲突,用少/无。
一个很有用的工具是带 –conflict 选项的 git checkout。
这会重新检出文件并替换合并冲突标记。 如果想要重置标记并尝试再次解决它们的话这会很有用。
可以传递给 –conflict 参数 diff3 或 merge(默认选项)。
如果传给它 diff3,Git 会使用一个略微不同版本的冲突标记: 不仅仅只给你 “ours” 和 “theirs” 版本,同时也会有 “base” 版本在中间来给你更多的上下文。$ git checkout --conflict=diff3 hello.rb
$ git config --global merge.conflictstyle diff3
git checkout 命令也可以使用 --ours
和 --theirs
选项,这是一种无需合并的快速方式,你可以选择留下一边的修改而丢弃掉另一边修改。
$ git log --oneline --left-right HEAD...MERGE_HEAD
# 它会只显示任何一边接触了合并冲突文件的提交。
$ git log --oneline --left-right --merge
# 如果你运行命令时用 -p 选项代替,你会得到所有冲突文件的区别。
这种叫作“组合式差异”的格式会在每一行给你两列数据。
如果我们解决冲突再次运行 git diff,我们将会看到同样的事情,但是它有一点帮助。
也可以在合并后通过 git log 来获取相同信息,查看冲突是如何解决的。
如果你对一个合并提交运行 git show 命令 Git 将会输出这种格式,
或者你也可以在 git log -p(默认情况下该命令只会展示还没有合并的补丁)命令之后加上 –cc 选项。$ git log --cc -p -1
最简单且最好的解决方案是移动分支到你想要它指向的地方。 git reset --hard HEAD~
,这会重置分支指向所以它们看起来像这样:
这个方法的缺点是它会重写历史,在一个共享的仓库中这会造成问题的。 查阅 变基的风险来了…
到目前为止我们介绍的都是通过一个叫作 “recursive” 的合并策略来正常处理的两个分支的正常合并。
如果你希望 Git 简单地选择特定的一边并忽略另外一边而不是让你手动解决冲突,你可以传递给 merge 命令一个 -Xours
或 -Xtheirs
参数。git merge -Xours mundo
这个选项也可以传递给我们之前看到的 git merge-file 命令, 通过运行类似 git merge-file –ours 的命令来合并单个文件。
如果想要做类似的事情但是甚至并不想让 Git 尝试合并另外一边的修改, 有一个更严格的选项,它是 “ours” 合并 策略。 $ git merge -s ours mundo
例如,假设你有一个分叉的 release 分支并且在上面做了一些你想要在未来某个时候合并回 master 的工作。
与此同时 master 分支上的某些 bugfix 需要向后移植回 release 分支。
你可以合并 bugfix 分支进入 release 分支同时也 merge -s ours 合并进入你的 master 分支 (即使那个修复已经在那儿了)这样当你之后再次合并 release 分支时,就不会有来自 bugfix 的冲突。
子树合并的思想是你有两个项目,并且其中一个映射到另一个项目的一个子目录,
$_PS: 是否类似github fork 工作方式。
$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote --no-tags
$ git checkout -b rack_branch rack_remote/master
# read-tree读取一个分支的根目录树到当前的暂存区和工作目录里。
# 先切回你的 master 分支,将 rack_back 分支拉取到我们项目的 master 分支中的 rack 子目录。
$ git read-tree --prefix=rack/ -u rack_branch
$ git checkout rack_branch
$ git pull
$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
这给我们提供了一种类似子模块工作流的工作方式,但是它并不需要用到子模块.
另外一个有点奇怪的地方是,当你想查看 rack 子目录和 rack_branch 分支的差异——你必须使用 git diff-tree 来和你的目标分支做比较:git diff-tree -p rack_branch
git diff-tree -p rack_remote/master
$_PS: 略。没用到
正如它的名字“重用记录的解决方案(reuse recorded resolution)”所示,它允许你让 Git 记住解决一个块冲突的方法, 这样在下一次看到相同冲突时,Git 可以为你自动地解决它。
git config --global rerere.enabled true
有几种情形下这个功能会非常有用。
$_PS: 略。没有那么高级的需求,还是每次merge安全些(也没有很不方便)
所以,如果做了很多次重新合并,或者想要一个主题分支始终与你的 master 分支保持最新但却不想要一大堆合并, 或者经常变基,打开 rerere 功能可以帮助你的生活变得更美好。
它能显示任何文件中每行最后一次修改的提交记录。git blame -L 69,82 Makefile
这其中有一个很有意思的特性就是你可以让 Git 找出所有的代码移动。git blame -C -L 141,153 GITPackUpload.m
git bisect start
git bisect bad
git bisect good v1.0
git bisect good
git bisect bad
git bisect reset
事实上,如果你有一个脚本在项目是正常的情况下返回 0,在不正常的情况下返回非 0,你可以使 git bisect 自动化这些操作。
git bisect start HEAD v1.0
git bisect run test-error.sh
子模块允许你将一个 Git 仓库作为另一个 Git 仓库的子目录。 它能让你将另一个仓库克隆到自己的项目中,同时还保持提交的独立。
[submodule "DbConnector"]
path = DbConnector
url = https://github.com/chaconinc/DbConnector
$_PS: 看起来是会有一些使用难度。看情况使用(不是每个人都了)
git submodule add https://github.com/chaconinc/DbConnector
git diff --cached DbConnector
当你在克隆这样的项目时,默认会包含该子模块目录,但其中还没有任何文件git submodule init
用来初始化本地配置文件,
而 git submodule update
则从该项目中抓取所有数据并检出父项目中列出的合适的提交。
如果给 git clone 命令传递 --recurse-submodules
选项,它就会自动初始化并更新仓库中的每一个子模块, 包括可能存在的嵌套子模块。
如果你已经克隆了项目但忘记了 –recurse-submodules,git submodule update --init --recursive。
git diff --submodule
,就会看到子模块被更新的同时获得了一个包含新添加提交的列表。git config --global diff.submodule log
git submodule update --remote DbConnector
此命令默认会假定你想要更新并检出子模块仓库的 master 分支。 不过你也可以设置为想要的其他分支。
既可以在 .gitmodules 文件中设置 (这样其他人也可以跟踪它),也可以只在本地的 .git/config 文件中设置git config -f .gitmodules submodule.DbConnector.branch stable
如果你设置了配置选项 status.submodulesummary,Git 也会显示你的子模块的更改摘要:git config status.submodulesummary 1
默认情况下,git pull 命令会递归地抓取子模块的更改,如上面第一个命令的输出所示。 然而,它不会 更新 子模块。git submodule update --init --recursive
git push 命令接受可以设置为 “check” 或 “on-demand” 的 –recurse-submodules 参数。 如果任何提交的子模块改动没有推送那么 “check” 选项会直接使 push 操作失败。git push --recurse-submodules=check
git push --recurse-submodules=on-demand
子模块遍历
有用的别名
切换分支
从子目录切换到子模块
bundle 命令会将 git push 命令所传输的所有内容打包成一个二进制文件
# 一个名为 repo.bundle 的文件,该文件包含了所有重建该仓库 master 分支所需的数据。
git bundle create repo.bundle HEAD master
git clone repo.bundle repo
## 如果你在打包时没有包含 HEAD 引用,你还需要在命令后指定一个 -b master 或者其他被引入的分支, 否则 Git 不知道应该检出哪一个分支。
git bundle create commits.bundle master ^9a466c5
# bundle verify 命令可以检查这个文件是否是一个合法的 Git 包,是否拥有共同的祖先来导入。
git bundle verify ../commits.bundle
# 查看这边包里可以导入哪些分支,同样有一个命令可以列出这些顶端:
git bundle list-heads ../commits.bundle
# 使用 fetch 或者 pull 命令从包中导入提交。 这里我们要从包中取出 master 分支到我们仓库中的 other-master 分支:
git fetch ../commits.bundle master:other-master
replace 命令可以让你在 Git 中指定 某个对象 并告诉 Git:“每次遇到这个 Git 对象时,假装它是 其它对象”。
我们可以将新历史推送到新项目中,当其他人克隆这个仓库时,他们仅能看到最近两次提交以及一个包含上述说明的基础提交。
现在我们将以想获得整个历史的人的身份来初次克隆这个项目。
$_PS: 略。不懂意义何在/多大
Git 拥有一个凭证系统来处理这个事情。 下面有一些 Git 的选项:
git config --global credential.helper cache
“store” 模式可以接受一个 –file <path>
参数,可以自定义存放密码的文件路径(默认是 ~/.git-credentials )。
“cache” 模式有 –timeout <seconds>
参数,可以设置后台进程的存活时间(默认是 “900”,也就是 15 分钟)。
git config --global credential.helper 'store --file ~/.my-credentials'
Git 甚至允许你配置多个辅助工具。
[credential]
helper = store --file /mnt/thumbdrive/.git-credentials
helper = cache --timeout 30000
$_PS: 略。只需知道,上面的凭证存储的3+2种模式就好